emacs markdown 代码块的快速选择

这个markdown模式和之前一篇org代码块快速选择类似。也是模仿了vim那样按下 Ctrl+c S 键就进入一个选择模式,然后按下 j k 可以用来选择上下文不同的代码块,按 y或者enter 选择复制文本,按 q 退出。

我在其中加个小改变,就是添加一段判断当前是否在emacs client打开当前,然后决定是否添加一段映射到markdown-quick-select-save-selection-to-file的代码。

(when (and (boundp 'server-buffer-clients) server-buffer-clients)
  (define-key markdown-quick-select-temp-keymap (kbd "s") 'markdown-quick-select-save-selection-to-file)
)

这是我的个人需求,emacs本身已经有org了,markdown对很多用emacs的人来说,只能是org的简易版。所以我的markdown经常是在emacs ”外部“的打开,也就是在终端中emacsclient打开md文件。

通过添加这个代码,我就可以在用emacsclient的时候直接用s快捷键,把md中的code block提取出来保存在当前目录中,用来快速使用md中的代码。这个功能也可以帮助我快速从llm生成的markdown文件中拷贝代码。

;;;;; markdown-quick-select-mode
(defvar markdown-quick-select-temp-keymap nil)
(defvar markdown-quick-select-restrict-mode nil)  ; 用来标记是否启用了限制模式
(defvar markdown-quick-select-original-keymap nil)  ; 保存原始的 keymap

(defun markdown-quick-select-toggle-restrict-keys ()
  "切换限制按键的状态,按下 C-c S 时启用或关闭限制模式,仅在 markdown-mode 中生效。"
  (interactive)
  (if (not (derived-mode-p 'markdown-mode))
      (message "此功能仅在 markdown-mode 中可用。")
    (if markdown-quick-select-restrict-mode
        (markdown-quick-select-enable-all-keys)  ; 如果限制模式已启用,按下时解除限制
      (markdown-quick-select-disable-other-keys))))  ; 否则启用限制模式


(defun markdown-quick-select-copy-code-block ()
  "拷贝当前代码块到剪贴板。"
  (interactive)
  (let ((code-block-bounds (markdown-get-code-block-bounds)))
    (if code-block-bounds
        (let ((start (car code-block-bounds))
              (end (cdr code-block-bounds)))
          (kill-ring-save start end)
          (message "已拷贝当前代码块"))
      (message "未找到代码块。"))))

(defun markdown-quick-select-disable-other-keys ()
  "屏蔽所有其他按键,仅允许 j/k 控制光标上下移动,并禁用输入,仅在 markdown-mode 中生效。"
  (interactive)
  (if (not (derived-mode-p 'markdown-mode))
      (message "此功能仅在 markdown-mode 中可用。")
    ;; 保存当前的 keymap 到 markdown-quick-select-original-keymap
    (setq markdown-quick-select-original-keymap (current-local-map))

    ;; 创建一个临时的 keymap,继承自原始的 markdown-mode keymap
    (setq markdown-quick-select-temp-keymap
          (let ((map (make-sparse-keymap)))
            (set-keymap-parent map markdown-mode-map)
            map))

    ;; 只允许 'j', 'k' 控制上下移动
    (define-key markdown-quick-select-temp-keymap (kbd "j") 'markdown-select-src-block-content-next)
    (define-key markdown-quick-select-temp-keymap (kbd "k") 'markdown-select-src-block-content-prev)

    ;; 将 M-w、y、Enter 键映射为复制当前代码块
    (define-key markdown-quick-select-temp-keymap (kbd "M-w") 'markdown-quick-select-copy-code-block)
    (define-key markdown-quick-select-temp-keymap (kbd "y") 'markdown-quick-select-copy-code-block)
    (define-key markdown-quick-select-temp-keymap (kbd "RET") 'markdown-quick-select-copy-code-block)

    (define-key markdown-quick-select-temp-keymap (kbd "q") 'markdown-quick-select-toggle-restrict-keys)

    (when (and (boundp 'server-buffer-clients) server-buffer-clients)
      (define-key markdown-quick-select-temp-keymap (kbd "s") 'markdown-quick-select-save-selection-to-file))

    ;; 使用临时 keymap
    (use-local-map markdown-quick-select-temp-keymap)

    ;; 启用只读模式,禁用其他输入
    (setq buffer-read-only t)
    (setq markdown-quick-select-restrict-mode t)  ;; 设置标记为限制模式已启用
    (message "键盘被限制,只有 j/k 可用,按 C-c S 解除限制。")))

(defun markdown-get-code-block-bounds ()
  "获取当前 Markdown 代码块的起始和结束位置。"
  (save-excursion
    (let ((start (save-excursion
                   (if (re-search-backward "```" nil t)
                       (progn
                         (forward-line 1)
                         (point))
                     nil)))
          (end (save-excursion
                 (if (re-search-forward "```" nil t)
                     (point-at-bol)
                   nil))))
      (if (and start end)
          (cons start end)
        nil))))

(defun markdown-select-src-block-content-next ()
  "搜索下一个 code block,跳转到位置并全选内容,排除代码块标记。"
  (interactive)
  (let ((start (point)))
    ;; 搜索下一个代码块开始
    (if (re-search-forward "```" nil t)
        (progn
          ;; 跳过语言标记行
          (forward-line 1)
          ;; 搜索代码块结束
          (let ((end (save-excursion
                       (re-search-forward "```" nil t)
                       (point-at-bol))))
            (push-mark (point) t t)
            (goto-char end)
            (activate-mark)))
      ;; 如果没有找到下一个 code block,则给出提示
      (message "没有找到下一个 code block"))))

(defun markdown-select-src-block-content-prev ()
  "搜索上一个 code block,跳转到位置并全选内容,排除代码块标记。"
  (interactive)
  (let ((start (point)))
    ;; 搜索上一个代码块结束
    (if (re-search-backward "```" nil t)
        (progn
          ;; 向前搜索对应的代码块开始
          (if (re-search-backward "```" nil t)
              (progn
                ;; 跳过语言标记行
                (forward-line 1)
                (let ((begin (point))
                      (end (save-excursion
                             (re-search-forward "```" nil t)
                             (point-at-bol))))
                  (push-mark (point) t t)
                  (goto-char end)
                  (activate-mark)))
            (message "没有找到对应的代码块开始"))
          )
      ;; 如果没有找到上一个 code block,则给出提示
      (message "没有找到上一个 code block"))))

(defun markdown-select-src-block-content ()
  "选中当前 Markdown 模式中的代码块内容,不包括代码块标记。"
  (interactive)
  (save-excursion
    (let ((begin (save-excursion
                   (re-search-backward "```" nil t)
                   (forward-line 1)
                   (point)))
          (end (save-excursion
                 (re-search-forward "```" nil t)
                 (point-at-bol))))
      (goto-char begin)
      (set-mark (point))
      (goto-char end)
      (activate-mark))))


(defun markdown-quick-select-save-selection-to-file (filename)
  "将当前选中的内容保存到指定的文件,文件名通过用户输入获得。"
  (interactive "F保存文件: ")
  (if (use-region-p)
      (let ((region-text (buffer-substring-no-properties (region-beginning) (region-end))))
        (with-temp-file filename
          (insert region-text))
        (message "选中的内容已保存到 %s" filename))
    (message "没有选中的内容可保存。")))

(defun markdown-quick-select-enable-all-keys ()
  "恢复所有按键并解除输入限制,仅在 markdown-mode 中生效。"
  (interactive)
  (if (not (derived-mode-p 'markdown-mode))
      (message "此功能仅在 markdown-mode 中可用。")
    ;; 恢复原始的 keymap
    (use-local-map markdown-quick-select-original-keymap)
    ;; 解除只读模式,允许输入
    (setq buffer-read-only nil)
    (setq markdown-quick-select-restrict-mode nil)  ;; 设置标记为限制模式已解除
    (message "已恢复所有按键和输入功能。")))